import sys
from xml.etree import ElementTree

import pyglet
from copy import copy
from gamelib import actions, pathfinding, magic, items, effects, abilities, traits

from gamelib.options import options

# list of sprites usable by player characters
female_character_sprites = [
    'person-f-green.png',
]
male_character_sprites = [
    'person-m-brown.png',
]

# XXX belt may take a number of items, should also be able to take a scabbard
# XXX maybe several types of belts - one with sword scabbard, one with dagger
# XXX one, one with just storage. What about bandoliers?
# XXX quiver or bandolier on shoulder

'''
head: hat, helmet, circlet, crown
neck: necklace
shoulder: bandolier (stores specific items), quiver
chest: armor, shirt, robe
wrists: bracelet, buckler shield
hands: gloves, gauntlets
right_hand: wielded weapon, wand
left_hand: another weapon, second hand for two-handed item, shield
waist: belt (stores specific items), scabbard, belt with dagger (scabbard?holster?)
legs: armor, pants, skirt
feet: shoes, boots
left_right/right_ring
'''

class Body(object):
    slots = 'head neck shoulder chest wrists hands left_hand right_hand waist legs feet left_ring right_ring'.split()
    slot_labels = [s.replace('_', ' ').title() for s in slots]
    slot_dict = dict(zip(slots, slot_labels))

    # equipment, armorcalculations
    def __init__(self):
        for slot in self.slots:
            setattr(self, slot, None)

    # reimplement using dict API (base class too?)
    def get(self, slot):
        return getattr(self, slot, None)

    def equip(self, item, slot=None):
        '''Slot is usually from the slots list, but might be "hand" or "ring".
        '''
        slot = slot or item.slot
        if slot not in self.slots:
            raise NotImplementedError('XXX handle "%s" as a slot spec'%slot)
        setattr(self, slot, item)

    def get_action_options(self):
        l = []
        for slot in self.slots:
            item = getattr(self, slot, None)
            if item is not None:
                l.extend(item.get_action_options())
        return l

    def get_armour_value(self, character):
        t = 0
        for slot in self.slots:
            item = getattr(self, slot, None)
            if item is not None:
                # XXX check for skill and modify value received
                t += item.armour_value + item.magic_bonus
        return t

    def list_items(self):
        l = []
        for slot in self.slots:
            item = getattr(self, slot, None)
            if item is not None:
                label = slot.replace('_', ' ').title()
                l.append((label, item))
        return l

    def to_xml(self, root):
        r = ElementTree.SubElement(root, 'body')
        r.tail = '\n'
        for slot in self.slots:
            item = getattr(self, slot, None)
            if item is None: continue
            s = ElementTree.SubElement(r, slot)
            s.tail = '\n'
            item.to_xml(s)

    @classmethod
    def from_xml(cls, tag):
        body = cls()
        for element in tag.getchildren():
            item = items.from_xml(element.find('item'))
            body.equip(item, element.tag)
        return body

# Descriptions of the various skills
skill_descriptions = dict(
    bashing = dict(
        label = 'Bashing',
        description = 'Physical combat without weapons.'
    ),
    weapon_sword = dict(
        label = 'Sword Combat',
        description = 'Sword combat.',
    ),
    weapon_bow = dict(
        label = 'Bow Combat',
        description = 'Bow combat.',
    ),
    weapon_staff = dict(
        label = 'Staff Combat',
        description = 'Staff combat.',
    ),
    weapon_mace = dict(
        label = 'Mace Combat',
        description = 'Mace combat.',
    ),
    spell_attack = dict(
        label = 'Attack Magic',
        description = 'Spells which cause damage.',
    ),
    spell_heal = dict(
        label = 'Healing Magic',
        description = 'Spells which heal damage.',
    ),
)

class Character(pyglet.event.EventDispatcher):
    '''
    - gender and cclass are pretty much ignored
    - name and sprite_name are used by the player to identify the character
    - position is the current map cell position
    - equipment is a list of items which are currently equipped
    - strength etc are basic stats used in action calculations
    - health/mana/actions are used during encounters so we record the max value to restore to
    - view_distance_dark isn't used yet but will dictate how far the character can
      see in the dark without light
    - spells is a list of Spell instances
    - skills is a dict of {skill name: skill level} where skill level is 0-10
    - carried is a list of items which are in the character's pack
    - abilities are intended to be special abilities the character has
    - effects are intended to be special effects applied to the character
    - traits grant special modifiers in certain situations (like low-light vision)
    '''
    def __init__(self, gender, cclass, name, position, sprite_name, equipment=[],
            strength=None, dexterity=None, constitution=None, intelligence=None,
            wisdom=None, charisma=None, max_health=None, max_mana=None,
            max_actions=None, mana=None, health=None, experience=0,
            view_distance_dark=0, spells=None, skills={},
            actions_remaining=None, carried=None, abilities=None,
            effects=None, traits=None):

        self.gender = gender
        self.cclass = cclass
        self.name = name
        self.position = position
        self.sprite_name = sprite_name

        # set all the bazillions of stats
        self.strength = strength
        self.dexterity = dexterity
        self.constitution = constitution
        self.intelligence = intelligence
        self.wisdom = wisdom
        self.charisma = charisma
        self.max_mana = max_mana
        self.max_health = max_health
        self.max_actions = max_actions
        self.view_distance_dark = view_distance_dark

        self.experience = experience

        # these may be supplied if we're loading a game, otherwise just
        # default them
        if mana is None: mana = max_mana
        self.mana = mana
        if health is None: health = max_health
        self.health = health
        if actions_remaining is None: actions_remaining = max_actions
        self.actions_remaining = max_actions

        # for inventory...
        self.body = Body()

        # load out equipment
        for slot in equipment:
            self.body.equip(equipment[slot], slot)

        # spells known
        self.spells = spells or []

        # skills learnt
        self.skills = {}
        for k in skills:
            if k not in skill_descriptions:
                raise KeyError('Unknown skill %r'%k)
            self.skills[k] = skills[k]

        # for carrying stuff...
        self.carried = carried or []

        # for special abilities
        self.abilities = abilities or []

        # for special effects
        self.effects = effects or []

        # for special traits
        self.traits = traits or []

        # XXX BUFFS AND DEBUFFS WOULD BE:
        # duration/type/stat affected and how much
        # for starters lets say a char can only be affected by
        # one of each kind of poison/disease/curse or something

        # XXX Ehtirno: is alignment something we want to have in here?
        # XXX Ehtirno: or does the storyline imply "good"?
        # XXX <rj>: yeah - let's leave it that way ... for now ;)

    def to_xml(self, root):
        m = ElementTree.SubElement(root, 'character',
            gender=self.gender,
            cclass=self.cclass,
            name=self.name,
            position='%s,%s'%(self.position),
            sprite_name=self.sprite_name,
            strength=str(self.strength),
            dexterity=str(self.dexterity),
            constitution=str(self.constitution),
            intelligence=str(self.intelligence),
            wisdom=str(self.wisdom),
            charisma=str(self.charisma),
            max_mana=str(self.max_mana),
            max_health=str(self.max_health),
            max_actions=str(self.max_actions),
            view_distance_dark=str(self.view_distance_dark),
            experience=str(self.experience),
            mana=str(self.mana),
            health=str(self.health),
            actions_remaining=str(self.actions_remaining),
        )
        m.tail = '\n'

        self.body.to_xml(m)

        for name, list in [('carried', self.carried), ('spells', self.spells),
                ('effects', self.effects), ('abilities', self.abilities),
                ('traits', self.traits)]:
            r = ElementTree.SubElement(m, name)
            r.tail = '\n'
            for item in list:
                item.to_xml(r)

        # XXX might want to make skills more complex to include usage / training info
        # XXX / speciality
        r = ElementTree.SubElement(m, 'skills')
        r.tail = '\n'
        for k, v in self.skills.items():
            s = ElementTree.SubElement(r, 'skill', name=k, value=str(v))
            s.tail = '\n'

    @classmethod
    def from_xml(cls, tag):
        skills = {}
        for s in tag.findall('skills/skill'):
            skills[s.get('name')] = int(s.get('value'))

        ch = cls(
            tag.get('gender'),
            tag.get('cclass'),
            tag.get('name'),
            tuple(map(int, tag.get('position').split(','))),
            tag.get('sprite_name'),

            strength=int(tag.get('strength')),
            dexterity=int(tag.get('dexterity')),
            constitution=int(tag.get('constitution')),
            intelligence=int(tag.get('intelligence')),
            wisdom=int(tag.get('wisdom')),
            charisma=int(tag.get('charisma')),
            max_mana=int(tag.get('max_mana')),
            max_health=int(tag.get('max_health')),
            max_actions=int(tag.get('max_actions')),
            view_distance_dark=int(tag.get('view_distance_dark')),
            experience=int(tag.get('experience')),
            mana=int(tag.get('mana')),
            health=int(tag.get('health')),
            actions_remaining=int(tag.get('actions_remaining')),

            skills = skills,
            spells = [magic.from_xml(s) for s in tag.findall('spells/spell')],
            carried = [items.from_xml(s) for s in tag.findall('carried/item')],
            effects = [effects.from_xml(s) for s in tag.findall('effects/effect')],
            abilities = [abilities.from_xml(s) for s in tag.findall('abilities/ability')],
            traits = [traits.from_xml(s) for s in tag.findall('traits/trait')],
        )
        ch.body = Body.from_xml(tag.find('body'))
        return ch

    def start_new_level(self, level):
        '''This is invoked when a level has just been initialised.

        "level" is a gamelib.level.Level instance.
        '''
        for ability in self.abilities:
            ability.start_new_level(level)

    def start_new_turn(self, level):
        '''This is invoked when the character is starting a new turn.

        "level" is a gamelib.level.Level instance.
        '''
        self.actions_remaining = self.max_actions

        for ability in self.abilities:
            ability.start_new_turn(level)

        # mana regeneration
        if self.mana < self.max_mana:
            add = max(self.wisdom, self.intelligence)
            self.mana = min(self.mana + add, self.max_mana)

    def get_action_options(self):
        '''List all the possible Actions this character may take. Someone else
        will filter them.
        '''
        l = [actions.MoveTo(), actions.BashAttack()]
        if options.god:
            l.append(actions.Zot())
        l.extend(self.body.get_action_options())
        for spell in self.spells:
            l.append(spell.action_class(spell))
        l.extend(self.abilities)
        return l

    def get_ranged_attack(self, level, cell, other):
        '''Get the best ranged attack option for attacking other in cell.
        '''
        iff = self.friend_or_foe(other)
        # check for melee spells
        # XXX how to choose preferable spell?
        prefs = []
        for spell in self.spells:
            if spell.mana_cost > self.mana: continue
            if spell.action_cost > self.actions_remaining: continue
            if spell.for_friendly != iff: continue
            if issubclass(spell.action_class, actions.RangedMagic):
                prefs.append(spell.action_class(spell))

        # now check for wielded weapon attack options
        weapon = self.body.get('right_hand')
        if weapon and weapon.action_cost <= self.actions_remaining:
            for action in weapon.get_action_options():
                if action.for_friendly != iff: continue
                if isinstance(action, actions.RangedAttack):
                    prefs.append(action)

        print 'get_ranged_attack', prefs

        # figure which one we'll use
        for attack in prefs:
            ok = attack.check(level, self, cell, other)
            if ok and attack.chance > 0:
                return attack

    def get_melee_attack(self, level, cell, other):
        '''Get the best melee attack option for attacking other in cell.
        '''
        iff = self.friend_or_foe(other)

        # check for melee spells
        prefs = []
        for spell in self.spells:
            if spell.mana_cost > self.mana: continue
            if spell.action_cost > self.actions_remaining: continue
            if spell.for_friendly != iff: continue
            if issubclass(spell.action_class, actions.MeleeMagic):
                prefs.append(spell.action_class(spell))

        # now check for wielded weapon attack options
        weapon = self.body.get('right_hand')
        if weapon and weapon.action_cost <= self.actions_remaining:
            for action in weapon.get_action_options():
                if action.for_friendly != iff: continue
                if isinstance(action, actions.MeleeAttack):
                    prefs.append(action)

        # add the default
        prefs.append(actions.BashAttack())

        # figure which one we'll use
        for attack in prefs:
            ok = attack.check(level, self, cell, other)
            if ok and attack.chance > 0:
                return attack

    def get_armour_value(self):
        # XXX some bodies might have an innate armour value too
        return self.body.get_armour_value(self)

    def __repr__(self):
        return '<%s %s health=%s>'%(self.__class__.__name__, self.name, self.health)

    def has_trait(self, name):
        # XXX this is kinda lame - specifying by arbitrary string could be
        # cause for problems later
        for trait in self.traits:
            if name == trait.name: return True
        return False

    def attack_missed(self, attack):
        '''An attack on this character has missed.
        "attack" is an actions.Attack instance.
        '''
        # XXX this might allow an attack of opportunity or movement or...
        self.dispatch_event('on_character_attack_miss', self, attack)

    def apply_damage(self, damage):
        '''Apply damage or healing to this character.
        '''
        print self, 'damaged', damage
        old_health = self.health
        self.health -= damage

        if self.health > self.max_health:
            self.health = self.max_health

        self.dispatch_event('on_character_damage', self, old_health, damage)

        if self.is_dead():
            self.dispatch_event('on_character_death', self)

    def find_accessible_cells(self, level):
        '''Figure which cells are accessible for this character.
        '''
        current_cell = level.get_cell(*self.position)

        # don't visit the None cell :)
        seen = {None: 9999999}

        def find_neighbors(cell, remain):
            if current_cell.position != cell.position:
                # check cost
                # XXX fix to use real cell cost when we have cell costs
                cost = 1
                if cost > remain:
                    return []
                remain -= cost
                # if there's already a cheaper path to this cell then pass
                if cell in seen and remain < seen[cell]:
                    return []
                l = [cell]
            else:
                l = []

            # ok, visit this cell
            seen[cell] = remain
            if not remain:
                return l

            # left
            n = level.get_neighbor(cell, level.LEFT)
            if n and not n.get('impassable'):
                l.extend(find_neighbors(n, remain))
            # diagonal up left
            m = level.get_neighbor(n, level.UP)
            if m and not m.get('impassable'):
                l.extend(find_neighbors(m, remain))
            # diagonal down left
            m = level.get_neighbor(n, level.DOWN)
            if m and not m.get('impassable'):
                l.extend(find_neighbors(m, remain))

            # right
            n = level.get_neighbor(cell, level.RIGHT)
            if n and not n.get('impassable'):
                l.extend(find_neighbors(n, remain))
            # diagonal up right
            m = level.get_neighbor(n, level.UP)
            if m and not m.get('impassable'):
                l.extend(find_neighbors(m, remain))
            # diagonal down right
            m = level.get_neighbor(n, level.DOWN)
            if m and not m.get('impassable'):
                l.extend(find_neighbors(m, remain))

            # up
            n = level.get_neighbor(cell, level.UP)
            if n and not n.get('impassable'):
                l.extend(find_neighbors(n, remain))

            # down
            n = level.get_neighbor(cell, level.DOWN)
            if n and not n.get('impassable'):
                l.extend(find_neighbors(n, remain))
            return l

        return find_neighbors(current_cell, self.actions_remaining)

    def is_dead(self):
        return self.health <= 0

    def is_active(self):
        # XXX there might be other situations where the character is not active
        # disabling effects? 0 hp = unconcious perhaps... many options here
        return self.health > 0

    def move_to(self, position, cost):
        '''Return boolean whether the move happened.
        '''
        if not self.use_action_points(cost):
            return False

        # move and apply the cost
        old_position = self.position
        self.position = position
        self.dispatch_event('on_character_move', self, old_position, position)
        return True

    def use_action_points(self, cost):
        '''Attempt to use "cost" action points.

        Return boolean whether it was possible.
        '''
        if self.actions_remaining < cost:
            return False
        self.actions_remaining -= cost
        self.dispatch_event('on_character_action_used', self, self.actions_remaining, cost)
        return True

    def use_mana(self, cost):
        '''Attempt to use "cost" mana points.

        Return boolean whether it was possible.
        '''
        if self.mana < cost:
            return False
        self.mana -= cost
        self.dispatch_event('on_character_mana_used', self, self.mana, cost)
        return True

Character.register_event_type('on_character_action_used')
Character.register_event_type('on_character_mana_used')
Character.register_event_type('on_character_move')
Character.register_event_type('on_character_damage')
Character.register_event_type('on_character_death')
Character.register_event_type('on_character_attack_miss')

